/**
 * The contents of this file are subject to the license and copyright detailed in the LICENSE and NOTICE files at the root of the source tree and available online at
 * http://www.askosi.org/license.html
 * 
 */
package be.destin.skos.core;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;

import be.destin.rdf.changes.ChangeManager;
import be.destin.skos.search.SearchResult;
import be.destin.util.Util;

/**
 * This is the entry point to ASKOSI (Application Services for Knowledge Organization and System Integration).
 * 
 * The method SkosManager.getInstance(configurationDirectory) is necessary to get access to the Schemes defined in a specific Configuration Directory.
 * Multiple instances of the SkosManager (and therefore multiple configuration directories) can coexist but they are impervious to each other.
 * 
 * The most usefuls methods are:
 * 
 * @author Christophe Dupriez
 *
 */
public class SkosManager {

    /** log4j logger */
    private static Logger log = null;

   public static final String ApplicationProxyParam = "ApplicationProxy";

   private Map<String/*list code*/,SchemeInterface> schemes = new ConcurrentHashMap<String,SchemeInterface>();
   private Map<String/*uri*/,String> schemesURIs = new ConcurrentHashMap<String,String>();
   
   private boolean baseSchemesInitialized = false; 

   private MetaScheme metaScheme = null;
   /** "about" code of the virtual Scheme containing the list of schemes loaded by this ASKOSI instance. */
   public final static String aboutMetaScheme = "metaScheme";

   private SchemeInterface prefixes;
   /** "about" code of the ConceptScheme containing the list of known prefixes for this ASKOSI instance. Prefixes are qualifiers in front of a reference to a concept. They are suffixed with an underscore. Example: solo_ instrument_flute or paper_ issn_1234_5678 */
   public final static String aboutPrefixes = "prefixes";

   private SchemeInterface suffixes;
   /** "about" code of the ConceptScheme containing the list of known suffixes for this ASKOSI instance. Suffixes are qualifiers following the reference to a concept. They are prefixed with an underscore. Example: instrument_flute _2 or substance_1234567 _poisoning */
   public final static String aboutSuffixes = "suffixes";
/*
   private SchemeInterface subfields;
   public final static String aboutSubfields = "subfields";
*/
   private SchemeInterface externals;
   /** "about" code of the ConceptScheme containing the list of Schemes NOT managed by ASKOSI but that are used to control metadata (to help transition to integral ASKOSI integration in a system) */
   public final static String aboutExternals = "externals";

   private SchemeInterface languages = null;
   /** "about" code of the ConceptScheme containing the list of all languages. Usually based on ISO 2 letters code. Collections like "userLanguage" are often defined to restrict this list for specific situations.*/
   public final static String aboutLanguages = "language";  // Singular, not plural

   private SchemeInterface applications;
   /** "about" code of the ConceptScheme containing the list of applications documented by this ASKOSI instance. */
   public final static String aboutApplications = "applications";

   public String applicationProxy = "/proxy/";
   
   private LinkedList<String>definedApplications;
   private TreeMap<String,String>sortedApplicationURLs;

   private SchemeInterface roles;
   /** "about" code of the ConceptScheme containing the list of possible "roles" (indexes using the ConceptScheme as a search criteria). */
   public final static String aboutRoles = "roles";

   private LinkedList<String>definedRoles;

   private SchemeInterface relationQualifiers;
   /** "about" code of the ConceptScheme containing the list of allowed Qualifiers to Relations between Concepts. For example: partitive */
   public final static String aboutRelationQualifiers = "relationqualifiers";
   private LinkedList<String>definedRelationQualifiers;

   private SchemeInterface categories;
   /** "about" code of the ConceptScheme containing the list of known categories of records from different applications containing references to Concepts */
   public final static String aboutCategories = "categories";

   private SchemeInterface profiles;
   /** ASKOSI users profiles with different update rights and viewing preferences. A given user may have access to different profiles */
   public final static String aboutProfiles = "profiles";
   
   private SchemeInterface users;
   /** ASKOSI users */
   public final static String aboutUsers = "users";
   
   private SchemeInterface sources = null;
   /** "about" code of the ConceptScheme containing the list of allowed Sources for Terms within Concepts. For example: INN, USAN, etc. */
   public final static String aboutSources = "sources";  // Singular, not plural
   
   private SchemeInterface status;
   /** "about" code of the ConceptScheme containing the list of allowed Status for the ASKOSI update workflow */
   public final static String aboutStatus = "status";

   private HashMap<String,UrlList> searches = null;
   
   private ChangeManager changeManager = null;
   
   private String configDirectory;

   private final HashMap<String/*application*/,String/*application root*/> urlRoots = new HashMap<String,String>();

   /** When displaying the result of search (or autocomplete), prefix to a synonym matching the search criteria and redirecting to the preferred term */
   public final static String synonymPrefix = " \u21D0"; // &lArr;
   
   /** Maps configuration directories to corresponding SkosManager instances */
   private static final Map<String,SkosManager> openedDirs = new ConcurrentHashMap<String,SkosManager>();

   private SkosManager (String confDirectory) { //confDirectory is already normalyzed by caller
	   super();
	   this.configDirectory = confDirectory;
	   this.metaScheme = new MetaScheme(); // The difficulty to create the very first scheme and concept in this scheme!
	   this.metaScheme.managerInstance = this;
	   this.metaScheme.conceptRepresentation = new Concept(this.metaScheme);
	   this.metaScheme.setAbout(aboutMetaScheme);
	   this.putScheme(this.metaScheme);
	   SkosManager previous = openedDirs.put(confDirectory, this);
	   if (previous != null) {
		   log.error("SkosManager instance opened twice for the same configuration directory?");
	   }
	   this.metaScheme.putTitle("ConceptScheme", null, TermList.anyLanguage, null);
   }
   
/**
 * This procedure is the essential entry point to every ASKOSI services.
 * It identifies the set of schemes one wishes to access using the directory where their configurations are stored.
 * 
 * @param configDirectory	the precise path (case sensitive) where the configurations of ASKOSI Concept Schemes are stored. Usually, the same directory for all applications sharing those schemes.
 * @return	the SkosManager instance that can be used for all ASKOSI services accesses.
 */
   public static SkosManager getInstance (String configDirectory) {
	   if (configDirectory == null) configDirectory = ""; // Current directory: rather dangerous!
	   if (!configDirectory.isEmpty()) {
		   if (File.separatorChar != '/') { // Change all "/" by the effective separator of current OS (Windows is also case insensitive)
			   configDirectory = Util.replaceAllNoRegExp(configDirectory.toLowerCase(), "/", File.separator);
		   }
		   char last = configDirectory.charAt(configDirectory.length()-1);
		   if (last != File.separatorChar) { // � la Unix or � la Windows...
			   configDirectory = configDirectory + File.separatorChar;
		   }
	   }
	   if (log == null) { // TODO: Clarifier comment initialiser log4j pour des modules ind�pendants...
		   String log4jConfProp = configDirectory+"log4j.properties";
		   PropertyConfigurator.configure(log4jConfProp);
		   log = Logger.getLogger(SkosManager.class);
		   log.info("ASKOSI logging installed using "+log4jConfProp);
	   }
	   SkosManager aManager = openedDirs.get(configDirectory);
	   if (aManager == null) {
		   aManager = getManagerInstance(configDirectory);
	   }
	   return aManager;
   }
 
   private static synchronized SkosManager getManagerInstance(String configDirectory) {
	   SkosManager aManager = openedDirs.get(configDirectory);
	   if (aManager != null) return aManager; // already done!

	   return new SkosManager(configDirectory);	   
   }

/**
 *TODO: List of external sources, list of suffixes (for an application?), list of prefixes (for an application?)...
 *TODO: HOW TO AUTOMATE RELOAD OF A LIST WHEN ITS SOURCE CHANGED (new author, new thesaurus entry?)  
 */

/**
 * Concept expansion function, allows specification of traversal costs, used by
 * the semantic closeness measure, currently for the core relationship types.
 * Returns an array of ConceptRelative objects.
 * NOT IMPLEMENTED and previous implementors have disappeared!
 * @param uri
 * @param costBT
 * @param costNT
 * @param costRT
 * @return An array of {@Concept
public ConceptRelative[] expandConcept(String uri, long costBT, long costNT, long costRT)
*/

/**
 * Get all Concepts directly related to the specified Concept. Returns an array of ConceptRelative objects.
 * NOT IMPLEMENTED and previous implementors have disappeared!
 * @param uri
 * @return

public ConceptRelative[] getAllConceptRelatives(String uri)
*/

/*
 * Get the specified Concept. Returns a single Concept object.
 */
public Concept[] getConcept(String uri) {
	Concept aConcept = getScheme_Concept(uri);
	if (aConcept == null) return null;
	Concept[] anArray = new Concept[1];
	anArray[0] = aConcept;
	return anArray;
} 

/**
 * @param abouts
 * @return
 */
public Collection<Concept> getConcept(Collection<String> abouts) {
	if (abouts == null) return null;
	HashSet<Concept> result = new HashSet<Concept>();
	for (String about: abouts) {
		Concept aConcept = getScheme_Concept(about);
		if (aConcept == null) continue;
		result.add(aConcept);
	}
	return result;
} 

public ArrayList<Concept> getMatchingConcepts(Collection<SearchResult> matches) {
	if (matches == null) return null;
	ArrayList<Concept> result = new ArrayList<Concept>(); // Easy to browse?
	for (SearchResult aResult: matches) {
		//aPref+aName+scopeNote+SchemeInterface.fieldSeparator+about
		/*int posSep = about.indexOf(SchemeInterface.fieldSeparator);
		if (posSep >= 0) {
			Concept aConcept = getScheme_Concept(about.substring(posSep+1));
			if (aConcept == null) continue;
			result.add(aConcept);
		}*/
		Concept aConcept = getScheme_Concept(aResult.getAbout());
		if (aConcept == null) continue;
		result.add(aConcept);
	}
	return result;
}

/*
 * Get the specified ConceptScheme. Returns a single ConceptScheme object.
 */
public ConceptScheme getConceptScheme(String uri) {
	SchemeInterface aScheme = openScheme(uri);
	if (aScheme == null) return null;
	if (aScheme instanceof CollectionScheme) {
		String schemeCode = ((CollectionScheme)aScheme).getDefaultScheme();
		if (schemeCode.endsWith(""+SchemeInterface.separator))
			schemeCode = schemeCode.substring(0, schemeCode.length()-1);
		aScheme = getScheme(schemeCode);
		if (aScheme == null) return null;
	}
	if (aScheme instanceof NotationScheme) return ((NotationScheme)aScheme).getPrimary();
	if (aScheme instanceof ConceptScheme) return (ConceptScheme) aScheme;
	return null;
}

/*
 * Get all ConceptSchemes supported by the service. Returns an array of ConceptScheme objects.
 */
public ConceptScheme[] getConceptSchemes() {
	LinkedList<ConceptScheme> result = new LinkedList<ConceptScheme>();
	for (SchemeInterface aScheme : schemes.values()) {
		if (aScheme instanceof ConceptScheme) result.add((ConceptScheme) aScheme);
	}
	return result.toArray(new ConceptScheme[0]);
} 

public Concept getExternal(String externalCode) {
	if (externals == null) return null;
	return externals.getConcept(externalCode);
}
                                                
/**
 * @return the externals
 */
public SchemeInterface getExternals() {
	return externals;
}

/*
 * Match a string to literal values in the SKOS entry vocabulary (preferred and
 * alternate labels), returns array of triples where the match occurs. Allows
 * use of MySQL Boolean query operators in the match. By returning triples
 * instead of Concepts it is clearer where the match occurs
 */
public /*RDFTriple[]*/ TermList getKeywordMatch(String keyword, boolean includeNPT) {
	//TODO: implement!
	return null;
}

/**
 * @return the prefixes
 */
public SchemeInterface getPrefixes() {
	return prefixes;
}

/**
 * @param key: code of the list to access
 * @return
 */
public SchemeInterface getScheme(String key) {
	if (key == null) return null;
	return schemes.get(SkosUtil.normalizeAbout(key).toLowerCase());
}

/**
 * @return the languages
 */
public SchemeInterface getLanguages() {
	return languages;
}

/**
 * @param languages the languages to set
 */
public void setLanguages(SchemeInterface languages) {
	this.languages = languages;
}

/**
 * Finds a concept by its code, validating it against a list of schemes it may belong.
 * Concept from the Prefix or the Suffix schemes (conceptCode_ or _conceptCode)are always allowed. 
 * 
 * @param value	Code to translate
 * @param defScheme	Default scheme if the code is not prefixed by its scheme code. May be null.
 * @param schemes	List of the schemes to which the concept is allowed to pertain
 * @return	the Concept for the provided code
 */
public Concept getScheme_Concept(String fullHandle, SchemeInterface defaultScheme, List<? extends SchemeInterface> allowedSchemes, List<NotationScheme> notationSchemes) {
	if (fullHandle == null || fullHandle.length() == 0) return null;
	String entry = null;
	SchemeInterface list = null;
	if (fullHandle.charAt(fullHandle.length()-1) == SchemeInterface.separator) {
		if (fullHandle.charAt(0) == SchemeInterface.separator) {
/*
			if (fullHandle.length() > 2) {
				list = subfields;
				entry = fullHandle.substring(1,fullHandle.length()-1);
			} // else we have "__" and this is not a subfield!
 */
		} else {
			list = prefixes;
			entry = fullHandle.substring(0,fullHandle.length()-1);
		}
	}
	else {
		int posSep = fullHandle.indexOf(SchemeInterface.separator);
		if (posSep < 0) {
			entry = fullHandle;
			list = defaultScheme;
		}
		else {
			if (posSep == 0) {
				entry = fullHandle.substring(1);
				if (entry.length() > 0) {
					list = suffixes;
				}
			} else if ((posSep+1) == fullHandle.length()) {
				entry = fullHandle.substring(0,posSep);
				if (entry.length() > 0) list = prefixes;
			} else {
				if (defaultScheme != null && !(defaultScheme instanceof DynamicScheme)) {
					Concept aConcept = defaultScheme.getConcept(fullHandle);
					if (aConcept != null) return aConcept;
				}
				entry = fullHandle.substring(posSep+1);
				String listCode = fullHandle.substring(0,posSep);
				if (allowedSchemes == null && notationSchemes == null) {
					list = openScheme(listCode);
					if (list != null && (list.getClass() == NoScheme.class)) {
						list = null;
					}
				} else {
					list = null;
					if (SkosManager.aboutPrefixes.equalsIgnoreCase(listCode)) {
						list = prefixes;
					} else if (SkosManager.aboutSuffixes.equalsIgnoreCase(listCode)) {
						list = suffixes;
/*
					} else if (SkosManager.aboutSubfields.equalsIgnoreCase(listCode)) {
						list = subfields;
*/
					} else if (defaultScheme != null && listCode.equalsIgnoreCase(defaultScheme.getAbout())) {
						list = defaultScheme;
					} else {
						if (allowedSchemes != null) {
							for (SchemeInterface aScheme : allowedSchemes) {
								if (listCode.equalsIgnoreCase(aScheme.getAbout())) {
									list = aScheme;
									break;
								}
							}
						}
						if (list == null && notationSchemes != null) {
							for (SchemeInterface aScheme : notationSchemes) {
								if (listCode.equalsIgnoreCase(aScheme.getAbout())) {
									list = aScheme;
									break;
								}
							}
						}
					}
				}

				if (list == null && defaultScheme != null && (defaultScheme instanceof DynamicScheme)) {
					Concept aConcept = defaultScheme.getConcept(fullHandle);
					if (aConcept != null) return aConcept;
				}
			}
		}
	}
	if (list == null) return null;
	if (entry == null) return null;
	log.debug("getScheme_Concept: "+list.getAbout()+'_'+entry+"="+list.getConcept(entry));
	return list.getConcept(entry); 
}

public Concept getScheme_Concept(String fullHandle) {
	return getScheme_Concept(fullHandle,(ConceptScheme)null, null, null);
}

public Concept getScheme_Concept(String fullHandle, String defListCode) {
	SchemeInterface aList = null;
	if (defListCode != null && defListCode.length() > 0) {
		aList = getScheme(defListCode);
		if (aList != null && (aList.getClass() == NoScheme.class) )
			aList = null;
	}
	return getScheme_Concept(fullHandle, aList, null, null);
}

public SchemeInterface getScheme_ (String fullHandle) {
	int i = fullHandle.indexOf(SchemeInterface.separator);
	if (i < 0) return getScheme(fullHandle);
	else if (i==0) return suffixes;
	else if ((i+1)==fullHandle.length()) return prefixes;
	else return getScheme(fullHandle.substring(0,i));
}

/**
 * @return the schemes
 */
public Map<String, SchemeInterface> getSchemes() {
	return schemes;
}

/**
 * @return the suffixes
 */
public SchemeInterface getSuffixes() {
	if (suffixes == null) return null;
	if (!(suffixes instanceof ConceptScheme)) return null;
	return suffixes;
}

/**
 * @return the subfields property
public SchemeInterface getSubfields() {
	if (subfields == null) return null;
	if (!(subfields instanceof ConceptScheme)) return null;
	return subfields;
}
 */

/**
 * FACET project API
 * 
 */
/*
 *    Get Concepts at the �top� of the hierarchies for specified ConceptScheme - returns an array of Concept.
 */
public Concept[] getTopmostConcepts(String uri) {
	SchemeInterface aList = this.getScheme(uri);
	if (aList == null) return null;
	if (aList.getClass() == NoScheme.class) {
		return (new Concept[0]);
	}
	else {
		if (aList instanceof ConceptScheme) {
			if ( ! ( (ConceptScheme) aList).isHierarchized()) {
				return aList.getConcept().toArray(new Concept[0]);
			}
		}
		HashSet<Concept> result = new HashSet<Concept>();
		// TODO: Should get top concepts from an SQL query for Dynamic schemes...
		for (Concept aChild : aList.getConcept()) {
			if (aChild.isTopConcept()) result.add(aChild);
		}
		return result.toArray(new Concept[0]);
	}
}

private synchronized boolean syncInitApplication() {
	if (this.baseSchemesInitialized) return false; // already done!

    SchemeFactory aFactory = new SchemeFactory(this);
	aFactory.loadConfig (aboutMetaScheme);
	
	if (prefixes == null || (prefixes.getClass() == NoScheme.class))
		prefixes = openScheme(aboutPrefixes);
	if (suffixes == null || (suffixes.getClass() == NoScheme.class))
		suffixes = openScheme(aboutSuffixes);
	if (languages == null || (languages.getClass() == NoScheme.class))
		languages = openScheme(aboutLanguages);
	if (externals == null || (externals.getClass() == NoScheme.class))
		externals = openScheme(aboutExternals);
	if (profiles == null || (profiles.getClass() == NoScheme.class))
		profiles = openScheme(aboutProfiles);
	if (users == null || (users.getClass() == NoScheme.class))
		users = openScheme(aboutUsers);
	if (relationQualifiers == null || (relationQualifiers.getClass() == NoScheme.class))
		relationQualifiers = openScheme(aboutRelationQualifiers);
	if (sources == null || (sources.getClass() == NoScheme.class))
		sources = openScheme(aboutSources);
	if (status == null || (status.getClass() == NoScheme.class))
		status = openScheme(aboutStatus);
	if (roles == null || (roles.getClass() == NoScheme.class))
		roles = openScheme(aboutRoles);
	if (categories == null || (categories.getClass() == NoScheme.class))
		categories = openScheme(aboutCategories);
	if (applications == null || (applications.getClass() == NoScheme.class)) {
		applications = openScheme(aboutApplications);
		for (Concept anApp : applications.getConcept()) {
			String appAbout = anApp.getAbout();
			this.addSortedApplicationURL(appAbout,Util.slashIt(this.getApplicationProxy())+anApp.getAbout()+'/');
			if (anApp instanceof Application) {
				Application anApplication = (Application)anApp;
				this.addSortedApplicationURL(appAbout,anApplication.getBaseURL());
			} else {
				this.addSortedApplicationURL(appAbout,anApp.getUrl(TermList.anyLanguage));
			}
			if ( ! this.urlRoots.containsKey(anApp.getAbout()) ) { // Application to make known!
				UrlList listUrl = anApp.getUrl();
				if (listUrl != null) {
					String aUrl = listUrl.getLing(TermList.ENGLISH); // or the available language...
					if (aUrl != null) {
						this.urlRoots.put(anApp.getAbout(),aUrl);
					}
				}
			}
		}
	}
	this.baseSchemesInitialized = true;
	return true;
}

public void initApplication(String applicationID, String applicationRoot) {
		applicationID = applicationID.trim();
		if (applicationRoot != null && !applicationRoot.isEmpty()) {
			this.urlRoots.put(applicationID,applicationRoot);
		}
		if (!this.baseSchemesInitialized) {
			if (this.syncInitApplication()) { // load once other needed data
				for (Concept anApplication : applications.getConcept()) { // Check if cached usage should be loaded
					if (anApplication instanceof Application) {
						((Application)anApplication).loadUsages();
					}
				}				
			}
		}
}

/*
private void mergeApplicationScheme(String application, String aboutScheme, SchemeInterface mergedScheme) {
	SchemeInterface localScheme = openScheme(aboutScheme);
	int nbAdd = 0;
	if (localScheme != null && ! (localScheme.getClass() == NoScheme.class) ) {
		log.error("No previous scheme to add "+aboutScheme);
	} else {
		Collection<Concept> localConcepts = localScheme.getConcept();
		if (localConcepts != null) {
			for (Concept aPrefix : localConcepts) {
				mergedScheme.putConcept(aPrefix);
				nbAdd++;
			}
		}
		log.debug(nbAdd+" concepts merged in "+aboutScheme);
	}
}
*/

private synchronized SchemeInterface syncOpenScheme(String about) {
	SchemeInterface aScheme = getScheme(about);
	if (aScheme != null) return aScheme;
	
	SchemeFactory aFactory = new SchemeFactory(this);
	if (aFactory.loadConfig (about)) {
		aScheme = getScheme(about);
		if (aScheme == null) {
			aScheme = new NoScheme(this,about);
		}
		else {
			log.debug(aScheme.getSize()+" concepts loaded for scheme "+aScheme.getAbout()+" ("+aScheme.getTitle(TermList.anyLanguage)+")");
		}
	}
	else {
		aScheme = new NoScheme(this,about);
	}
	if (aScheme.getClass() == NoScheme.class) {
		log.debug("NoScheme="+about);
	}
	return aScheme;
}

public SchemeInterface openScheme(String about) {
	if (about == null || about.isEmpty()) return null;
	SchemeInterface aScheme = getScheme(about);
	if (aScheme == null) {
		aScheme = syncOpenScheme(about);
	}
	return aScheme;
}

/*
 * @param value: list definition to add
 * @return true if this a reload of an existing scheme
 */
public boolean putScheme(SchemeInterface scheme) {
	String schemeAbout = scheme.getAbout();
	if (scheme.getClass() != NoScheme.class) {
		metaScheme.putConcept(scheme.getConceptRepresentation());
	}
	String uri = ((NoScheme)scheme).toUriScheme();
	if (uri != null && !uri.isEmpty()) {
		schemesURIs.put(uri, schemeAbout);
		//scheme.getConceptRepresentation().putUrl(TermList.anyLanguage, uri); NO! NO! Already used by Display URL!!!
	}
	SchemeInterface currScheme = schemes.put(schemeAbout.toLowerCase(), scheme );
	if (currScheme != null && currScheme.getClass() != NoScheme.class) {
		log.info("Scheme "+schemeAbout+" is reloaded.");
		if (currScheme.getClass() != scheme.getClass()) {
			log.error("Scheme Class was"+currScheme.getClass().toString()+" and is now "+scheme.getClass().toString());
		}
		return true; // was there...
	}
	return false; // new scheme!
}

/*
 * Returns schemeAbout for a uri
 */
public String findSchemeByURI(String uri) {
	if (uri == null) return null;
	return schemesURIs.get(uri);
}

/**
 * Record the existence of a Scheme for later accesses.
 * Ensures the "about" code of the Scheme is set to key.
 * @param key	"about" code of the scheme to record
 * @param scheme	scheme object to record
 */
public void putScheme(String key, SchemeInterface scheme) {
	scheme.setAbout(key); /* ensure coherence */
	putScheme(scheme);
}

/**
 * @param externals the externals to set
 */
public void setExternals(SchemeInterface externals) {
	this.externals = externals;
}

/**
 * @param prefixes the prefixes to set
 */
public void setPrefixes(SchemeInterface prefixes) {
	this.prefixes = prefixes;
}

/**
 * @param suffixes the suffixes to set
 */
public void setSuffixes(SchemeInterface suffixes) {
	this.suffixes = suffixes;
}

/**
 * Takes any text String, detects concept references within it and translates them with the best prefLabel for the language specified.
 * Concept references "candidates" are any word containing an underscore. Unfound "candidates" are left as is.
 * 
 * @param input	Text to analyze
 * @param language	User language
 * @param defaultList	Not useful: null recommended. Default scheme if the code is not prefixed by its scheme code (but contains an underscore!)
 * @param conceptSchemes TODO
 * @param notationSchemes TODO
 * @param prefixSuffix	Does prefix (prefixCode_) or suffix (_suffixCode) should be detected?
 * @return	The text with concept codes translated for the user.
 */
public String toStringScheme_Concept(String input, String language, SchemeInterface defaultList, List<? extends SchemeInterface> conceptSchemes, List<NotationScheme> notationSchemes, boolean prefixSuffix){
	// log.debug("Explain "+input+" in "+language+", default="+defaultList.toString());
	StringBuffer output = new StringBuffer();
	boolean subst = false;
	int precPos = 0;
	int pos = input.indexOf(SchemeInterface.separator);
	while (pos >= 0) {
		int i = pos-1;
		while (i >= 0 && Character.isJavaIdentifierPart(input.charAt(i)))
			i--;
		i++;
		output.append(input.substring(precPos, i));
		int j = pos+1;
		while (j < input.length() && Character.isJavaIdentifierPart(input.charAt(j)))
			j++;
		String fullHandle = input.substring(i,j);
		log.debug("toString "+fullHandle);
		boolean OK = prefixSuffix;
		if (!OK)
			if ( fullHandle.charAt(0) != SchemeInterface.separator
					&& fullHandle.charAt(fullHandle.length()-1) != SchemeInterface.separator)
				OK = true;
		if (OK) {
			Concept aConcept = getScheme_Concept(fullHandle, defaultList, conceptSchemes, notationSchemes);
			if (aConcept == null) output.append(fullHandle);
			else {
				output.append(aConcept.getPrefLabel(language, false));
				subst = true;
			}
		}
		precPos = j;
		pos = input.indexOf(SchemeInterface.separator, precPos);
	}
	if (subst) return output.append(input.substring(precPos)).toString();
	if (defaultList == null) return input;
	Concept aConcept = defaultList.getConcept(SkosUtil.normalizeAbout(input));
	if (aConcept == null) return input;
	return aConcept.getPrefLabel(language, false);
} 

/**
 * Takes any text String and returns a set of concept references within it.
 * Concept references "candidates" are any word containing an underscore. Unfound "candidates" are not returned.
 * 
 * @param input	Text to analyze
 * @param defaultList	Not useful: null recommended. Default scheme if the code is not prefixed by its scheme code (but contains an underscore!)
 * @param conceptSchemes TODO
 * @param notationSchemes TODO
 * @param prefixSuffix	Does prefix (prefixCode_) or suffix (_suffixCode) should be detected?
 * @return	Set of found Concepts
 */
public HashSet<Concept> toSetScheme_Concept(String input, SchemeInterface defaultList, List<? extends SchemeInterface> conceptSchemes, List<NotationScheme> notationSchemes, boolean prefixSuffix){
	// log.debug("Explain "+input+" in "+language+", default="+defaultList.toString());
	HashSet<Concept> resultSet = new HashSet<Concept>();
	if (input != null) {
		boolean subst = false;
		int precPos = 0;
		int pos = input.indexOf(SchemeInterface.separator);
		while (pos >= 0) {
			int i = pos-1;
			while (i >= 0 && Character.isJavaIdentifierPart(input.charAt(i)))
				i--;
			i++;
			int j = pos+1;
			while (j < input.length() && Character.isJavaIdentifierPart(input.charAt(j)))
				j++;
			String fullHandle = input.substring(i,j);
			log.debug("toSet "+fullHandle);
			boolean OK = prefixSuffix;
			if (!OK) {
				OK = fullHandle.charAt(0) != SchemeInterface.separator
				&& fullHandle.charAt(fullHandle.length()-1) != SchemeInterface.separator;
			}
			if (OK) {
				Concept aConcept = getScheme_Concept(fullHandle, defaultList, conceptSchemes, notationSchemes);
				if (aConcept != null) {
					if ( ! prefixSuffix ) {
						SchemeInterface currScheme = aConcept.getInScheme(); 
						OK = currScheme != this.prefixes && currScheme != this.suffixes;
					}
					if (OK) {
						resultSet.add(aConcept);
						subst = true;
					}
				}
			}
			precPos = j;
			pos = input.indexOf(SchemeInterface.separator, precPos);
		}
		if (!subst && defaultList != null) {
			Concept aConcept = defaultList.getConcept(SkosUtil.normalizeAbout(input));
			if (aConcept != null) {
				resultSet.add(aConcept);			
			}
		}
	}
	return resultSet;
} 

/**
 * Takes any text String and returns a set of the scheme_about codes of the concepts within it.
 * Concept references "candidates" are any word containing an underscore. Unfound "candidates" are not returned.
 * Prefixes and suffixes are always ignored.
 * 
 * @param input	Text to analyze
 * @return	Set of found Concepts
 */
public HashSet<String> toSetAbout(String input){
	// log.debug("Explain "+input+" in "+language+", default="+defaultList.toString());
	HashSet<String> resultSet = new HashSet<String>();
	int precPos = 0;
	int pos = input.indexOf(SchemeInterface.separator);
	while (pos >= 0) {
		int i = pos-1;
		while (i >= 0 && Character.isJavaIdentifierPart(input.charAt(i)))
			i--;
		i++;
		int j = pos+1;
		while (j < input.length() && Character.isJavaIdentifierPart(input.charAt(j)))
			j++;
		String fullHandle = input.substring(i,j);
		SchemeInterface aScheme = getScheme_ (fullHandle);
		if (aScheme != null) { 
			if (aScheme != this.prefixes && aScheme != this.suffixes) {
				resultSet.add(fullHandle);
			}
		}
		precPos = j;
		pos = input.indexOf(SchemeInterface.separator, precPos);
	}
	return resultSet;
} 

/**
 * @return the configuration directory for this SkosManager Instance
 */
public String getConfigDirectory() {
	return configDirectory;
}

/**
 * @return the ConceptScheme defining the applications for this SkosManager Instance 
 */
public ConceptScheme getApplications() {
	if (applications == null) return null;
	if (!(applications instanceof ConceptScheme)) return null;
	return (ConceptScheme)applications;
}

/**
 * @param applications the applications to set
 */
public void setApplications(SchemeInterface applications) {
	this.applications = applications;
}


/**
 * @return the roles
 */
public ConceptScheme getRoles() {
	if (roles == null) return null;
	if (!(roles instanceof ConceptScheme)) return null;
	return (ConceptScheme)roles;
}

/**
 * @param roles the roles to set
 */
public void setRoles(SchemeInterface roles) {
	this.roles = roles;
}

/**
 * @return the roles
 */
public ConceptScheme getRelationQualifiers() {
	if (relationQualifiers == null) return null;
	if (!(relationQualifiers instanceof ConceptScheme)) return null;
	return (ConceptScheme)relationQualifiers;
}

public void setRelationQualifiers(SchemeInterface relationQualifiers) {
	this.relationQualifiers = relationQualifiers;
}

/**
 * @return the roles
 */
public ConceptScheme getStatus() {
	if (status == null) return null;
	if (!(status instanceof ConceptScheme)) return null;
	return (ConceptScheme)status;
}

public void setStatus(SchemeInterface statuses) {
	this.status = statuses;
}

public String getApplicationRoot (String application) {
	ConceptScheme aScheme = this.getApplications();
	if (aScheme != null) {
		Concept aConcept = aScheme.getConcept(application);
		if (aConcept != null) {
			String url = aConcept.getUrl(TermList.anyLanguage);
			if (url != null && !url.isEmpty()) return url;
		}
	}
	return this.getUrlRoots(application);
}

public LinkedList<String> getDefinedApplicationsList() {
	if (definedApplications == null) {
		ConceptScheme theApplications = getApplications();
		if (theApplications == null) return null;
		definedApplications = new LinkedList<String>();
		for (Concept aConcept : theApplications.getConcept()) {
			definedApplications.add(aConcept.getAbout());
		}
	}
	return definedApplications;
}

public LinkedList<String> getDefinedRolesList() {
	if (definedRoles == null) {
		ConceptScheme theRoles = getRoles();
		if (theRoles == null) return null;
		definedRoles = new LinkedList<String>();
		for (Concept aConcept : theRoles.getConcept()) {
			definedRoles.add(aConcept.getAbout());
		}
	}
	return definedRoles;
}

public LinkedList<String> getDefinedRelationQualifiersList() {
	if (definedRelationQualifiers == null) {
		ConceptScheme theRelationQualifiers = getRelationQualifiers();
		if (theRelationQualifiers == null) return null;
		definedRelationQualifiers = new LinkedList<String>();
		for (Concept aConcept : theRelationQualifiers.getConcept()) {
			definedRelationQualifiers.add(aConcept.getAbout());
		}
	}
	return definedRelationQualifiers;
}

/**
 * @return the sortedApplicationURLs
 */
public TreeMap<String, String> getSortedApplicationURLs() {
	if (sortedApplicationURLs == null) sortedApplicationURLs = new TreeMap<String,String>();
	return sortedApplicationURLs;
}

/**
 * @param sortedApplicationURLs the sortedApplicationURLs to set
 */
public void setSortedApplicationURLs(
		TreeMap<String, String> sortedApplicationURLs) {
	this.sortedApplicationURLs = sortedApplicationURLs;
}

public void addSortedApplicationURL (String about, String url) {
	if (url == null || url.isEmpty()) return;
	int PDot = url.indexOf('{');
	if (PDot >= 0) {
		url = url.substring(0,PDot);
	}
	/* BAD IDEA !
	PDot = url.indexOf('?');
	if (PDot >= 0) {
		url = url.substring(0,PDot);
	} */
	url = Util.noSlashIt(url.toLowerCase());
	if (url == null) return;
	String key = Integer.toString(99999-url.length()); // Create a sort key with 5 characters with a number in decreasing order giving the URL length 
	TreeMap<String, String> sorter = getSortedApplicationURLs();
	char rank = 'Z';  // followed by a priority indicator
	while (rank >= ' ') {
		String completeKey = key+rank+url; // followed by the URL
		String currApp = sorter.get(completeKey);
		if (currApp == null) {
			sorter.put(completeKey,about);
			break;
		} else if (about.equalsIgnoreCase(currApp)) {
			// do nothing!
			break;
		} else {
			rank--;
		}
	}
}

public String findSortedApplicationURL (String url) {
	if (url == null) return null;
	url = url.trim();
	if (url.isEmpty()) return null;
	String workURL = Util.noSlashIt(url.toLowerCase());
	if (workURL == null) return null;
	for (Map.Entry<String,String> entry : this.getSortedApplicationURLs().entrySet()) {
		String template = entry.getKey().substring(6); // after 99999z... 
		if (workURL.startsWith(template)) {
			return entry.getValue()+SchemeInterface.separator+url.substring(template.length());
		}
	}
	return null;
}

/**
 * @param application to harvest
 * @return a list of SchemeUsage to be harvest for this application
 */
public SchemeUsageList getHarvestedUsage(String application) {
	SchemeUsageList toHarvest = new SchemeUsageList();
	for (SchemeInterface aScheme : this.getSchemes().values() ) {
		if (aScheme instanceof SchemeUsageInterface) {
			for (SchemeUsage aUsage : ((SchemeUsageInterface)aScheme).getUsage()) {
				if (aUsage.getApplication().equals(application)) {
					String harvestUrl = aUsage.getHarvestURL();
					if (harvestUrl != null && !harvestUrl.isEmpty()) {
						toHarvest.put(aUsage);
					}
				}
			}
		}
		
	}
	return toHarvest;
}

public HashMap<String,UrlList> getSearch() {
	return this.searches;
}

public UrlList getSearch(String application) {
	if (this.searches == null) return null;
	return this.searches.get(application);
}

/**
 * @return the categories
 */
public ConceptScheme getCategories() {
	if (categories == null) return null;
	if (!(categories instanceof ConceptScheme)) return null;
	return (ConceptScheme)categories;
}

/**
 * @param categories the categories to set
 */
public void setCategories(SchemeInterface categories) {
	this.categories = categories;
}

public ConceptScheme getProfiles() {
	if (profiles == null) return null;
	if (!(profiles instanceof ConceptScheme)) return null;
	return (ConceptScheme)profiles;
}

public void setProfiles(SchemeInterface profiles) {
	this.profiles = profiles;
}

public ConceptScheme getUsers() {
	if (users == null) return null;
	if (!(users instanceof ConceptScheme)) return null;
	return (ConceptScheme)users;
}

public void setUsers(SchemeInterface users) {
	this.users = users;
}

/**
 * @return the urlRoots
 */
public String getUrlRoots(String application) {
	if (urlRoots == null) return null;
	return urlRoots.get(application);
}

/**
 * @param input
 * @param language
 * @param defaultList
 * @param conceptSchemes TODO
 * @param notationSchemes TODO
 * @param prefixSuffix
 * @param alfaOnly TODO
 * @return
 */
public String toSortScheme_Concept(String input, String language, SchemeInterface defaultList, List<? extends SchemeInterface> conceptSchemes, List<NotationScheme> notationSchemes, boolean prefixSuffix, boolean alfaOnly){
	// log.debug("Explain "+input+" in "+language+", default="+defaultList.toString());
	StringBuffer output = new StringBuffer();
	boolean subst = false;
	int precPos = 0;
	int pos = input.indexOf(SchemeInterface.separator);
	while (pos >= 0) {
		int i = pos-1;
		while (i >= 0 && Character.isJavaIdentifierPart(input.charAt(i)))
			i--;
		i++;
		output.append(Util.noAccent(input.substring(precPos, i).toUpperCase()));
		int j = pos+1;
		while (j < input.length() && Character.isJavaIdentifierPart(input.charAt(j)))
			j++;
		String fullHandle = input.substring(i,j);
		log.debug("toString "+fullHandle);
		boolean OK = prefixSuffix;
		if (!OK)
			if ( fullHandle.charAt(0) != SchemeInterface.separator
					&& fullHandle.charAt(fullHandle.length()-1) != SchemeInterface.separator)
				OK = true;
		if (OK) {
			Concept aConcept = getScheme_Concept(fullHandle, defaultList, conceptSchemes, notationSchemes);
			if (aConcept == null) output.append(fullHandle);
			else {
				output.append(aConcept.getSortLabel(language, alfaOnly));
				subst = true;
			}
		}
		precPos = j;
		pos = input.indexOf(SchemeInterface.separator, precPos);
	}
	if (subst) return output.append(Util.noAccent(input.substring(precPos).toUpperCase())).toString();
	if (defaultList == null) return Util.noAccent(input).toUpperCase();
	Concept aConcept = defaultList.getConcept(SkosUtil.normalizeAbout(input));
	if (aConcept == null) return Util.noAccent(input).toUpperCase();
	return aConcept.getSortLabel(language, alfaOnly);
}

/**
 * @return
 */
public ChangeManager getChangeManager() {
	if (changeManager == null) {
		changeManager = new ChangeManager(this);
	}
	return changeManager;
}

/**
 * @return	a Scheme with one Concept per Scheme loaded in this instance of ASKOSI
 */
public MetaScheme getMetaScheme() {
	return metaScheme;
}

/**
 * @return the ConceptScheme with the known sources for the terms of the different ConceptSchemes
 */
public ConceptScheme getSources() {
	if (sources == null) return null;
	if (!(sources instanceof ConceptScheme)) return null;
	return (ConceptScheme)sources;
}

/**
 * No foreseen use.
 * Set the ConceptScheme with the known sources for the terms of the different ConceptSchemes
 * 
 * @param sources
 */
public void setSources(SchemeInterface sources) {
	this.sources = sources;
}

/** 
 * List configuration directories corresponding to opened SkosManager instances.
 * @return	Set of configuration directories. Beware: later accesses are lower/upper case dependent.
 */
public static Set<String> getInstancesList() {
	return openedDirs.keySet();
}

/**
* @return the applicationProxy
*/
public String getApplicationProxy() {
	return applicationProxy;
}

/**
* @param applicationProxy the applicationProxy to set
*/
public void setApplicationProxy(String applicationProxy) {
	if (applicationProxy != null && !applicationProxy.isEmpty()) {
		this.applicationProxy = applicationProxy.trim();
	}
}

}
